Reporte Hito 3: Tweets durante Juegos Olímpicos Río 2016¶

Juan Díaz, Cristián Herrera, Álvaro Toledo

CC5206 - Introducción a la Minería de Datos, 28 de Junio 2019.

Introducción¶

Día a día, una gran cantidad de información es transmitida por el mundo. Parte de esta información corresponde a la que las personas comparten a través de sus redes sociales. Facebook, Instagram y Twitter, entre otras, son una de las tantas redes sociales donde la gente comparte información tanto de su vida personal como de reflexiones y opiniones respecto a temas de toda índole. Siguiendo esta última idea, los eventos deportivos, en particular, suelen llamar el interés de una fracción importante de los usuarios en las redes sociales, concentrando gran parte del tráfico que se transmite durante las horas de los respectivos eventos.

Considerando lo anterior, cabe la pregunta de si, dado un evento deporitvo, podría existir alguna relación entre la actividad temporal en Twitter y el desarrollo de este, es decir, con los acontecimiento relevantes (highlights) que suceden en el evento. Si bien un solo tweet no sería capaz de aportar más información que la contenida en su texto y hora de envío, puede que el conjunto de tweets contenga información implícita sobre el desarrollo global del evento. Concretamente, la pregunta de investigación de este trabajo es:

¿Es posible generar un resumen temporal de un evento deportivo a partir únicamente del conjunto de tweets relacionados al evento?

Así, como caso de estudio particular, se analizará el tráfico de tweets realizados durante el periodo de los juegos olímpicos de Río 2016, tal de detectar y extraer tópicos relacionados a sucesos relevantes que acontecieron durante la realización de este evento. El objetivo de este análisis consiste en poder generar, a partir de estos tópicos detectados, resumenes capaces de transmitir tanto los resultados concretos de los eventos, como también la dinámica y la intensidad de su desarrollo, esperando enriquecer considerable la calidad de estos resumenes. Para lograr lo anterior, no obstante, es necesario responder las siguientes preguntas:

  1. ¿Se puede hacer clustering sobre este tipo de tweets tal de distinguir los principales tópicos?
  2. ¿Dado un cierto cluster, es posible obtener un tweet que represente/resuma el tópico del cluster?
  3. ¿Qué tanto variarán estos clusters durante el desarrollo de los eventos?

Estas preguntas finalmente, guiarán el trabajo de este proyecto y limitarán los alcances de este de acuerdo a los resultados que se obtengan.

Descripción de los datos¶

El dataset empleado en esta investigación consiste en un registro de aproximadamente 29.5 millones de tweets realizados entre el 11 y el 23 de Agosto del 2016 y que de alguna manera se relacionan al hashtag #Rio2016. Vale la pena notar que los juegos se realizaron durante el 5 y 21 de Agosto y que, de acuerdo a la documentación de Twitter, los tweets obtenidos mediante la API corresponden a solo un 1% de los tweets en el servidor. Aún así, los tweets contienen información de cerca de 300 eventos deportivos distintos, relacionados a los 28 deportes que conforman los juegos.

Ahora bien, los datos están contenidos en 2943 archivos json, donde cada uno contiene 10.000 tweets que corresponden a un cierto intervalo de tiempo (por lo general, ~4 minutos). Los tweets contenidos en estos datasets poseen 29 atributos, de los cuales, para este proyecto, solamente se consideran 6: el texto, el lenguaje, la ubicación de origen del tweet (respecto al perfil del usuario), la fecha y hora de emisión, los hashtags y los tags a usuarios. Por ejemplo, un tweet del dataset:

  • created_at: "Mon Aug 15 01:04:03 +0000 2016"
  • text: "That was absolutely beautiful my word! Wayde Van Niekerk Olympic champion and WR holder #Gold"
  • lang: en
  • location: "New York"
  • user_mentions: None
  • hastags: "Gold"

Para poder trabajar con los atributos mencionados anteriormente, se deben extraer a partir de los archivos json. Para reducir la cantidad de información a la hora de realizar la exploración incial, se optó por crear un nuevo DataFrame solamente con los atributos que se mencionaron anteriormente. Este DataFrame se consigue leyendo cada archvio .json y extrayendo los atributos mencionados a cada tweet. Posteriormente se transforma en un único archivo .pkl (pickle), conteniendo toda la información necesaria para el proyecto. De este modo se ejecuta el proceso de extracción de datos una única vez, a modo de reducir el tiempo utilizado en futuros algoritmos.

Exploración de los datos¶

Previo al pre-procesamiento de los datos, se realiza una exploración a través de estos para visualizar la información que contienen y la relevancia de esta para el proyecto. Para la exploración de datos se tomaron en consideración principalmente 3 días, correspondientes al 11,16 y 20 de Agosto de 2016 (330.000 tweets aprox.). En primera instancia, dado que la mayor parte de la información de cada tweet radica en su texto, se realizaron WordClouds tal observar la existencia de palabras predominantes en el dataset y si de algún modo se lograba representar de que se estaba hablando en estos intervalos. Como ejemplo, el siguiente WordCloud corresponde a las palabras más utilizadas durante el 16 de Agosto.

Como se puede observar, múltiples palabras y nombres aparecen en resultado, dando cuenta que númerosos tópicos se desarrollan y comentan día a día (e incluso hora). Esto indica que existe una gran dispersión respecto a los temas y tópicos que los usuarios comentan y discuten minuto a minuto en Twitter y que por tanto, como es de esperar, la extracción de tópicos relevantes requiere de técnicas más sofisticadas capaces de sortear esta dispersión que bien puede considerarse como ruido.

Por otro lado, vale la pena notar del resultado anterior que existe también la posibilidad que un evento que no este relacionado directamente con algún evento deportivo surja como tema predominante, como es el caso de "ZacEfron Surprise" que aparece en la parte inferior de la imagen. Esto, por supuesto, tiene relación con la sorpresa que Zac Efron realizó a la atleta Simone Biles el 16 de Agosto. Si bien la aparición de este tema en particular no es extraña, resulta interesante que temas anexos al desarrollo de las competencias sean relevantes respecto al tráfico de twitter. Esto podría considerarse como un ejemplo de que se podría extraer información aparte de los resultados concretos de los eventos.

Siguiendo con la exploración, otro aspecto se encontró analizando el comportamiento de los hashtags. En particular, se observó la variación existente entre distintos días e incluso entre diferentes horas de un mismo día. A continuación se pueden observar 3 gráficos que corresponden a los hashtags en los tres días mencionados anteriormente.

Así, se observa que existe una clara correspondencia entre el tráfico de twitter de un determinado intervalo de tiempo y el tráfico de twitter correspondiente. Por lo tanto, a la hora de querer analizar un evento en especifíco no es necesario analizar la totalidad del dataset, sino que es suficiente con enfocar el análisis al intervalo de tiempo al que este corresponde. Por otro lado, tamién se puede apreciar que la distribución de tweets no es uniforme respecto a los eventos deportivos, por lo que se puede decir con cierta seguridad que existen cierto eventos que atraen más la atención de los usuarios (por ejemplo, el futból).

Finalmente, para tener en consideración a la hora de realizar los trabajos futuros, se realiza un análisis a la distribución del lenguaje en el dataset tal de observar el idioma predominante en los tweets. A continuación se muestran los resultados de la predominancia de los idiomas en los 3 días mencionados anteriormente. Se puede observar que el inglés es el principal lenguaje utilizado en los tweets, seguido por el español y portugés (cabe destacar que el día del 20 de Agosto, el portugués aumento en uso debido a que ese día se jugó la Final de Fútbol, donde Brasil salió campeón). Teniendo esto en cuenta, resulta razonable para el trabajo considerar únicamente los tweets en inglés ya que aún así se mantendría un porcentaje importante del dataset.

,,

Preprocesamiento de los datos¶

En primer lugar, con el fin de facilitar los experimentos, algunos filtros son aplicados sobre el dataset. Siguiendo la consideración detallada en la sección anterior, se comienza por filtrar únicamente los tweets en inglés. Luego, siguiendo una metodología similar a la observada en trabajos como [1][2], se remueven aquellos tweets que corresponden a re-tweets (RT). La justificación de esto último radica en que mantener estos podría implicar la existencia de tweets repetidos en el dataset, lo que podría perjudicar la utiliación de clustering al alterar la densidad de estos.

Por otra parte, también con el fin de facilitar el desarrollo de los experimentos, se realizó un proceso de normalización de texto. Este consistió en colocar todos los carácteres del texto en minúsculas (lowercase) y remover de este las URLS, los emojis y los caracteres no alfanuméricos (como los puntos, comas, #, $, etc.). De este modo se tiene un nuevo DataFrame exclusivamente con el texto procesado y la fecha de creación. En particular, esto facilita su tokenizing y posterior transformación a la matriz tf-idf [3] (que se detallará a continuación).

Para poder realizar clustering con los tweets, y en particular k-means, se realiza un procedimiento de vectorización tal de posibilitar el posterior cálculo de distancia o bien, similaridad entre estos. Se utilizó la transformación TD-IDF (term frecuency-inverse document frecuency), y de esta forma se obtiene una representación de cada tweet del dataset en un vector de alta dimensionalidad. Luego, para calcular la similaridad/distancia entre los tweets, se utiliza la métrica distancia-coseno que corresponde a una medida de distancia entre dos vectores y no a su magnitud como la distancia Euclidiana. La distancia coseno sigue la ecuación:

Donde x e y corresponden a los vectores de cada tweet resultantes de la transformación TF-IDF. Esta distancia está acotada entre 0 y 1.

Ahora, nuevamente siguiendo una metodología similar a la utilizada en [3], utilizando 3 algoritmos para clasificar correctamente los tweets de ruido. Estos algortimos utilizan tanto K-means como DBSCAN para esta tarea, y remueven aproximadamente entre 35% a 50% de los tweets analizados. Para poder filtrar los tweets de ruido en el dataset, en primer lugar se crea una matriz de consenso. A partir de numerosas iteraciones de K-Means (usando como medida distancia coseno) la matriz de consenso es creada donde se registra mediante un sistema de votos, tal que cada vez que los puntos i y j son contenidos en el mismo cluster, se suma 1 a las casillas (i,j) y (j,i) de la matriz. Mientras mayor sea el valor (i, j) en la matriz, significa que estos dos tweets (ti y tj) son clasificados en el mismo cluster con mayor frecuencia.

La siguiente imagen muestra (en menor escala) los resultados que entrega este algoritmo utilizado para la clasificación de los tweets.

Así, se puede observar que en la matriz de consenso mientras mayor sea el valor (i, j) en la matriz (más amarillo en la imagen que muestra una matriz obtenida), significa que estos dos tweets (ti y tj) se encuentran en un mismo cluster con mayor frecuencia. Luego, los tweets son etiquetados como ruido si la suma de su fila en la matriz de consenso es menor al promedio de suma de fila entre todos los tweets. En general, este proceso elimina alrededor de un 50% de los tweets en el dataset, no obstante, tiene la desventaja que prioriza clusters de igual densidad por lo que posibles clusters con densidad menor al promedio son removidos.

Para evitar que se eliminen los tweets que están en clusters de baja densidad, se utiliza un algortimo basado en DBSCAN logrando preservar los datos que habrían sido perdidos. Variando el ε, se realizan múltiples iteraciones de clustering con DBSCAN sobre los datos. En una matriz de clasificación se registra la etiqueta de cada tweet en cada iteración. Cada casilla (i, j) representa la etiqueta (core, border, noise) del tweet i, en la iteración j. Existen 2 versiones de este algoritmo, una que utiliza directamente los tweets utilizados y otra que utiliza como métrica de distancia la matriz de consenso generada con el algoritmo 1 con K-means. Un tweet se considera ruido se fue clasificado como tal en más del 50% de las iteraciones realizadas.

Con esto se tienen los 3 distintos algoritmos para clasificar tweets como ruido. Por lo tanto, un tweet es definido como ruido si ha sido clasificado como tal en al menos 2 de los 3 algortimos usados. Finalmente se eliminan del dataset aquellos tweets que han sido clasificados como ruido según el criterio anteriormente mencionado.

Clustering de los datos y resultados obtenidos¶

Con los datos ya filtrados, se realiza el proceso de clustering teniendo en consideración lo observado en la matriz de consenso. Este proceso se realiza de forma discreta, debido a la gran cantidad de tweets a procesar (10.000 tweets). El clustering se realiza utilizando K-means, donde el valor de clusters es calculado para cada intervalo utilizando el método del codo. Es decir, el número de clusters utilizado en un intervalo no necesariamente va a ser el mismo que el utilizado para el intervalo siguiente o el anterior. Para ilustrar los resultados obtenidos al utilizar el algoritmo, se utilizarán solamente los tweets que se publicaron el 15 de Agosto entre las 01:15 AM UTC hasta las 1:55 AM UTC. El motivo principal de esto es porque en este intervalo de tiempo Usain Bolt había logrado su tercera medalla de oro en los 100 metros planos y Wayde van Niekerk rome el record mundial en los 400 metros planos, entre otros eventos. Así se podrá ver si el algoritmo utilizado logra identificar estos eventos a través de cluster, observar como se desarrolla la respuesta de los tweets frente a estos evento (y si se lográn detectar estos nuevos eventos) y extraer un resumen de cada cluster para los intervalos utilizados.

Las matrices mostradas a continuación corresponden a las matrices de similaridad obtenidas para cada intervalo utilizado.

Se puede observar a partir de todas las matrices obtenidas, que efectivamente se pueden identificar los clusters (que corresponden a los "cuadrados" en la diagonal) y que el número de clusters utilizados varía dependiende de cada intervalo, unos poseen solamente 3 clusters como el que corresponde al intervalor del UTC 01:49, o pueden ser 5 como en el intervalo del UTC 01:31. Otra cosa importante de destacar es que se logran identificar varios clusters que poseen un alto grado de similaridad, es decir los tweets que los componen se parecen mucho entr sí, como sería el caso del intervalo 01:14 y 01:20 por nombrar algunos. Al realizar un análisis de estos clusters extrayendo los tweets correspondientes a esos clusters, se logró encontrar que la razón por la cual poseen alta similaridad es porque son literalmente el mismo texto, repetidos varias veces a través del intervalo. A estos tweets se le denominaron tweets bots, ya que se repiten varias veces a lo largo de todo el clusters. De todas formas, esto indica que el algortimo fue capaz de agrupar todos los tweets correspondientes a un tweet bot específico en un cluster separado del resto, por lo que no afecta la matriz de similaridad del resto de los tweets.

Por otra parte también se logró extraer el tweet más cercano al centroide de su cluster respectivo, que es utilizado para resumir el contenido general de los tweets contenidos en ese cluster. A continuación se presentan los resultados obtenidos para los intervalos utilizados.

UTC 01:14:17¶

CLUSTER 0 | olympics olympicsolympics i love it

CLUSTER 1 | all about rio rio2016

CLUSTER 2 | 5 secretsbrazil doesntwant you to know aboutolympics

CLUSTER 3 | usain bolt wins his third olympic gold medal in the 100m

UTC 01:20:07¶

CLUSTER 0 | in the olympics

CLUSTER 1 | great news to wake up to thanks for making us proud waydedreamergold medal and a new world record rio2016

CLUSTER 2 | chinese diver proposesmarriageat olympics medal ceremonyone chinesediver shocks anotherby proposingto her duringan olympics m

CLUSTER 3 | usain bolt wins his third olympic gold medal in the 100

UTC 01:26:04¶

CLUSTER 0 | wayde van niekerkwins gold sets new world record rio2016 olympics

CLUSTER 1 | skateboardingat the 2020 olympicsmay have one big problemweed

CLUSTER 2 | usain bolt wins olympic gold in 100m for third time

UTC 01:31:52¶

CLUSTER 0 | olympics

CLUSTER 1 | usainboltdoes it again

CLUSTER 2 | usain bolt that is all olympics

CLUSTER 3 | rio olympics2016 wayde van niekerkbreaksworld record to win olympic gold

CLUSTER 4 | rio2016 usain bolt wins 100m gold at the rio olympics

UTC 01:37:33¶

CLUSTER 0 | nice

CLUSTER 1 | wayde van niekerkbreaks michael johnsons17yearold world record on way to winning 400m olympic gold

CLUSTER 2 | super human usain bolt wins his third olympics 100m gold medal eight years after explodingto superstardom

CLUSTER 3 | five things to watch monday the rio olympic games entersits final week with stars of london 2012 looking to

UTC 01:43:35¶

CLUSTER 0 | five things to watch monday the rio olympic games enters its final week with stars of london 2012 looking to

CLUSTER 1 | 5 secrets brazil doesnt want you to know about olympics

CLUSTER 2 | wayde van niekerk waydedreamer wins gold and sets a new world record in the 400m race at rio2016

CLUSTER 3 | rio2016 usain bolt wins 100m gold at the rio olympics

CLUSTER 4 | nhl wire rio olympics indian gymnast dipa karmakar nearly medals inspires world

UTC 01:49:55¶

CLUSTER 0 | nice

CLUSTER 1 | gold rio2016 usainbolt

CLUSTER 2 | skateboardingat the 2020 olympicsmay have one big problemweed

CLUSTER 3 | rio olympicsusain bolt makes historywith third 100m gol

Visualización¶

Finalmente, para poder observar los clusters de manera gráfica, primero se debe realizar un re-escalado dimensional a la matriz tf-idf con los tweets filtrados. Como los datos fueron transformados a vectores de una alta dimensionalidad con la transformación, se vuelve prácticamente imposible visualizar el resultado del clustering directamente. Así, se entrena un modelo PCA para tomar la matriz tf-idf y reducirla a 2 dimensiones tal de poder utilizar esta nueva matriz para gráficar en un plano x-y los puntos con sus respectivas etiquetas obtenidas del clustering. Se mostrarán a continuación los clusters obtenidos para cada intervalo utilizado.

UTC 01:14:17¶

UTC 01:20:07¶

UTC 01:26:04¶

UTC 01:31:52¶

UTC 01:37:33¶

UTC 01:43:35¶

UTC 01:49:55¶

Se puede observar a través de estos gráficos que efectivamente se logra reducir la dimensionalidad de la matriz tf_idf para poder visualizar los clusters de manera gráfica, sin embargo no se logra observar de manera mas diferenciada los clusters en comparación a las matrices de similaridad, por lo que se prefiere utilizar las matrices por sobre este método. También se intento visualizar los datos utilizando TSNE para reducir la dimensionalidad, sin embargo los gráficos siguen sin entregar información mucho más clara que con las matrices de similaridad, sin embargo se logran visualizar los tweets bots agrupados como se ve en el ejemplo a continuación:

UTC 01:14:17¶

Análisis de resultados¶

Con respecto a los hitos anteriores, se logró observar que al incluir más algoritmos para la remoción de ruido se logra mejorar la coherencia que poseen los clusters detectados y por lo tanto se logra identificar más claramente los tópicos de cada intervalo.

Además como se mencionó anteriormente se lográn identificar clusters con tweets repetidos o tweets bots implicando en matrices de alta similaridad. En el intervalo mostrado de ejemplo (UTC 01:20:07), el tweet bot corresponde al cluster 2 de ese intervalo que sería chinese diver proposes marriage at olympics medal ceremony one chinese diver shocks another by proposing to

Por otra parte se puede observar la evolución de los tópicos a medida que pasa el tiempo, y como algunos tópicos se mantienen. Por ejemplo con los intervalores UTC 01:20:07 y 01:26:04:

UTC 01:20:07¶

CLUSTER 0 | in the olympics CLUSTER 1 | great news to wake up to thanks for making us proud waydedreamergold medal and a new world record rio2016 CLUSTER 2 | chinese diver proposesmarriageat olympics medal ceremonyone chinesediver shocks anotherby proposingto her duringan olympics m CLUSTER 3 | usain bolt wins his third olympic gold medal in the 100

UTC 01:26:04¶

CLUSTER 0 | wayde van niekerkwins gold sets new world record rio2016 olympics CLUSTER 1 | skateboardingat the 2020 olympicsmay have one big problemweed CLUSTER 2 | usain bolt wins olympic gold in 100m for third time

Se observa que se mantiene el tópico relacionado a Usain Bolt al pasar del primer intervalo al segundo, y también se logra observar que se genera un nuevo tópico correspondiente al nuevo recor mundial establecido por Wayde van Nieker. Además también se logran obserar en ambos intervalos la presencia de tweets bot, que son clasificados en su propio cluster sin afectar los otros.

Conclusiones¶

A partir de los resultados obtenidos y el análisis realizado se puede realizar varias conclusiones y extraer varios aprendizajes.

En primer lugar, se observa que el algoritmo de reducción de ruido permite reducir considerablemente el tamaño del dataset, mejorando la eficiencia y reduciendo el costo computacional de los clusterings, sin perjudicar considerablemente los tópicos contenidos inicialmente. Además se logra resolver el problema de los clusters de baja densidad evitando que se pueda perder información útil.

Ahora, en cuanto a los resultados del clustering, se observa que con los algoritmos de remoción de ruido se logran identificar de mejor manera los clusters e identificar mejor los tópicos. De forma más relevante, se observa la formación de clusters con buena similaridad, conformados por tweets que comparten una similaridad en cuanto a significado y no respecto a coincidencia de palabras. Por lo tanto, esto indica que las técnicas empleadas permiten la agrupación de tweets respecto a tópicos globales y significado. Además se logran identificar los clusters con una similaridad muy alta correspondiente a los tweets bots, logrando separar estos del resto de los clusters y evitando que estos afecten los tópicos del restosde los cluster.

En cuanto a las respuestas obtenidas a las preguntas de investigación realizadas al principio del informe se puede concluir:

  • Si es posible hacer clustering sobre tweets, logrando identificar tópicos principales.
  • Utilizando el tweet más cercano al centroide del cluster es posible obtener un tweet representativo del cluster.
  • Nuevos y diversos tópicos van apareciendo a medida que se procesan intervalos vecinos. Otros tópicos, se repiten sin cambiar mucho su contenido.

    # Bibliografía

[1] Dela Rosa K., Shah R., Lin B., Gershman A., Frederking R. Topical Clustering of Tweets. SWSM'10 (2011), Beijing.

[2] Alonso O., Shiells K. Timelines as Summaries of Popular Scheduled Events. IW3C2 (2013), Rio de Janeiro.

[3] Godfrey D., Johns C., Sadek C. A Case Study in Text Mining: Interpreting Twitter Data From World Cup Tweets. (2014) California.

Anexos (código)¶

In [ ]:
# -*- coding: utf-8 -*-

# K-MEANS NOISE REDUCTION
import pandas as pd
import re
import string


import numpy as np
from progressbar import ProgressBar
import random
from nltk.cluster import KMeansClusterer, cosine_distance

from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.cluster import DBSCAN


def clean_corpus(TWEET_DB, batch_size):
    """
    Realiza un procesamiento sobre el tweet corpus:
        - colocar texto en minúsculas (lowercase).
        - filtrar únicamente tweets en inglés (en).
        - filtrar tweets que correspondan a RT.
        - eliminar URLS de los tweets (https://...).
        - eliminar caracteres no-ascii.
        
    :param DataFrame TWEET_DATABASE:
        DataFrame con el database de tweets de un cierto intervalo de tiempo.
    
    :returns:
        DataFrame con el tweet_corpus procesado
    """
    
    tweet_corpus = pd.DataFrame(0,
                                index = TWEET_DB.index,
                                columns = [u'text', u'time'])

    bar = ProgressBar()
    j = 0
    for i in bar(TWEET_DB.index[:batch_size]):
        text = TWEET_DB.loc[i, 'text']
        idioma = TWEET_DB.loc[i, 'language']
        
        # filtrar
        if (text[0:2] == 'RT') or (idioma != 'en'):
            continue
        
        # colocar texto en lowercase
        text = text.lower()
        
        # eliminar caracteres no-ascii
        text = text.encode('ascii','ignore').decode('utf-8')
        
        # eliminar urls y puntuación
        text = re.sub('http://\S+|https://\S+', '', text)
        text = re.sub('[%s]' % re.escape(string.punctuation), '', text)
        
        # agregar al corpus
        tweet_corpus.loc[j, 'text'] = text
        tweet_corpus.loc[j, 'time'] = TWEET_DB.loc[i, 'created_at']
        j += 1
        
    
    tweet_corpus = tweet_corpus.iloc[:j, :]
    return tweet_corpus

def kmeans_consensus_matrix(BATCH, k_min, k_max, num_iters):
    """
    Realiza clustering con la matriz TF-IDF utilizando k-means:
        - Itera multiples veces con distintos numeros de clusters.
        - Retorna una matriz de concenso cuya casilla i,j indica cuantas veces
          los tweets i y j fueron etiquetados en el mismo cluster.
          
    :param list BATCH:
        lista de vectores tf-idf de los tweets.
    :param int k_min:
        numero minimo de clusters a probar.
    :param int k_max:
        numero maximo de clusters a probar.
    :param int num_iters:
        numero de iteraciones a realizar.
    
    :returns:
        matriz de concenso (numpy array)
    """
    
    batch_size = len(BATCH)
    CONSENSUS_MATRIX = np.zeros( (batch_size, batch_size) )
    
    
    RANDOM_SEED = 237
    random.seed(RANDOM_SEED)
    
    # lista de iteraciones a realizar
    NUM_CLUSTERS = list(range(k_min, k_max))*int( num_iters/(k_max-k_min+1) )
    NUM_CLUSTERS.sort()
    
    bar = ProgressBar()
    for k in bar(NUM_CLUSTERS):
        val = True
      
        while val:
          
            try:
                # realizar clustering
                kclusterer = KMeansClusterer(k, distance=cosine_distance,
                                             avoid_empty_clusters=False,
                                             repeats=7)

                CLUSTER_LABELS = kclusterer.cluster(BATCH,
                                                    assign_clusters=True)
                
                # agregar votos a la matriz de concenso
                for i in range(batch_size):
                    for j in range(batch_size):

                        if CLUSTER_LABELS[i] == CLUSTER_LABELS[j]:
                            CONSENSUS_MATRIX[i, j] += 1
                    
                # remover variables de clusters
                del kclusterer
                del CLUSTER_LABELS
                
                val = False
                
            except AssertionError:
                val = True
        
    # retornar matriz de concenso
    return CONSENSUS_MATRIX/float( len(NUM_CLUSTERS) )
            

def kmeans_noise(BATCH, CONSENSUS_MATRIX):
    """
    A partir de la matriz de concenso se identifican los twees de ruido
        
    :param list BATCH:
        lista de vectores tf-idf de los tweets.     
    :param numpy_array CONSENSUS_MATRIX:
        matriz de concenso generada con kmeans clustering.
    
    :returns:
        lista de index de los vectores etiquetados como ruido.
    """
    
    batch_size = len(BATCH)
            
    # NOISE FILTERING
    NOISE_TWEETS = []
    
    for i in range(batch_size):
        for j in range(batch_size):
            
            if CONSENSUS_MATRIX[i, j] < 0.1:
                CONSENSUS_MATRIX[i, j] = 0
          
    # promedio de suma de fila
    CONSENUS_AVERAGE = np.mean( np.sum(CONSENSUS_MATRIX, axis=0) )
    for i in range(batch_size):
        row_sum = np.sum( CONSENSUS_MATRIX[i, :] )
      
        if row_sum < CONSENUS_AVERAGE:
            NOISE_TWEETS.append(i)
        
    return NOISE_TWEETS


def dbscan_noise_v1(BATCH, e_min, e_max, num_iters):
    """
    Realiza clustering con la matriz TF-IDF usando DBSCAN:
        - Itera multiples veces utilizando distintos epsilon.
        - Retorna la lista de tweets clasificados como ruido.
    
    :param list BATCH:
        lista de vectores tf-idf de los tweets.
    :param int e_min:
        numero minimo de epsilon a probar.
    :param int e_max:
        numero maximo de epsilon a probar.
    :param int num_iters:
        numero de iteraciones a realizar.
    
    :returns:
        lista de index de los vectores etiquetados como ruido.
    """
    
    batch_size = len(BATCH)
    LABELS_MATRIX = np.zeros( (batch_size, num_iters) )
    
    EPS = np.linspace(e_min, e_max, num_iters)
    
    bar = ProgressBar()
    for i in bar(range(num_iters)):
        clusterer = DBSCAN(eps=EPS[i], min_samples=10, metric='cosine')
        
        CLUSTERING = clusterer.fit(BATCH)
        LABELS_MATRIX[:, i] = CLUSTERING.labels_
        
        del clusterer
        del CLUSTERING
        
    # NOISE CLASSIFICATION
    NOISE_TWEETS = []
    
    for i in range(batch_size):
        label_history = list(LABELS_MATRIX[i, :])
        noise_count = label_history.count(-1)
        
        if noise_count >= 0.5*num_iters:
            NOISE_TWEETS.append(i)
            
    return NOISE_TWEETS

def dbscan_noise_v2(BATCH, CONSENSUS_MATRIX, e_min, e_max, num_iters):
    """
    Realiza clustering con la matriz de consenso usando DBSCAN:
        - Itera multiples veces utilizando distintos epsilon.
        - Retorna la lista de tweets clasificados como ruido.
    
    :param list BATCH:
        lista de vectores tf-idf de los tweets.
    :param numpy_array CONSENSUS_MATRIX:
        matriz de concenso generada con kmeans.
    :param int e_min:
        numero minimo de epsilon a probar.
    :param int e_max:
        numero maximo de epsilon a probar.
    :param int num_iters:
        numero de iteraciones a realizar.
    
    :returns:
        lista de index de los vectores etiquetados como ruido.
    """
    
    batch_size = len(BATCH)
    LABELS_MATRIX = np.zeros( (batch_size, num_iters) )
    
    EPS = np.linspace(e_min, e_max, num_iters)
    DISTANCE_MATRIX = np.ones( (batch_size, batch_size) ) - CONSENSUS_MATRIX
    
    bar = ProgressBar()
    for i in bar(range(num_iters)):
        clusterer = DBSCAN(eps=EPS[i], min_samples=10, metric='precomputed')
        
        CLUSTERING = clusterer.fit(DISTANCE_MATRIX)
        LABELS_MATRIX[:, i] = CLUSTERING.labels_
        
        del clusterer
        del CLUSTERING
        
    # NOISE CLASSIFICATION
    NOISE_TWEETS = []
    
    for i in range(batch_size):
        label_history = list(LABELS_MATRIX[i, :])
        noise_count = label_history.count(-1)
        
        if noise_count >= 0.5*num_iters:
            NOISE_TWEETS.append(i)
            
    return NOISE_TWEETS

def true_noise(BATCH, NOISE_A1, NOISE_A2, NOISE_A3):
    """
    Identifica a los tweets que hayan sido clasificados como ruido en al menos
    dos de los tres algoritmos de filtro
    
    :param list BATCH:
        lista de vectores tf-idf de los tweets.
    :param list NOISE_A1:
        lista de indices de los tweets clasificados como ruido bajo el primer 
        algoritmo de filtro (kmeans)
    :param list NOISE_A2:
        lista de indices de los tweets clasificados como ruido bajo el segundo 
        algoritmo de filtro (DBSCAN)
    :param list NOISE_A3:
        lista de indices de los tweets clasificados como ruido bajo el tercer 
        algoritmo de filtro (DBSCAN-CONSENSUS MATRIX)
    
    :returns:
        lista de indices de los tweets clasificados como ruido
        
    """
    
    NOISE = []
    batch_size = len(BATCH)
    
    for i in range(batch_size):
        noise_count = NOISE_A1.count(i) + NOISE_A2.count(i) + NOISE_A3.count(i)
        
        if noise_count >= 2:
            NOISE.append(i)
    
    return NOISE


def final_clustering(TF_IDF):
    """
    Realiza clustering sobre los tweets, se utiliza el criterio del codo para
    obtener la cantidad de clusters.
    
    :param list TF_IDF:
        lista con los tf-idf de los tweets sobre los cuales hacer clustering.
    :param list NOISE:
        lista con los indices de los tweets clasificados como ruido.
        
    :returns:
        lista con las etiquetas del clustering.
    """
    
    batch_size = len(TF_IDF)
    
    RANDOM_SEED = 237
    random.seed(RANDOM_SEED)
    
    NUM_CLUSTERS = list(range(1,12))
    last_SE = 0
    
    bar = ProgressBar()
    for k in bar(NUM_CLUSTERS):
      
        val = True
        while val:
            try:
                kclusterer = KMeansClusterer(k, distance=cosine_distance,
                                             avoid_empty_clusters=False,
                                             repeats=11)

                CLUSTER_LABELS = kclusterer.cluster(TF_IDF, assign_clusters=True)
                
                val = False
                
            except AssertionError:
                val = True
                # remover variables de clusters
                del kclusterer
                del CLUSTER_LABELS
                
        MEANS = kclusterer.means()
        
        for label in range(k):
            SE = 0
            centroid = MEANS[label]
            
            for i in range(batch_size):
                if CLUSTER_LABELS[i] == label:
                    SE += cosine_distance(centroid, TF_IDF[i])
                    
        if k == 1:
            last_SE = SE
            
            del kclusterer
            del CLUSTER_LABELS
            continue
        
        if SE > last_SE*0.8:
            # retonar cluster del codo
            return [CLUSTER_LABELS, MEANS]
        else:
            last_SE = SE
            del kclusterer
            del CLUSTER_LABELS
            
        
In [ ]:
################################################################################
############################# NOISE REDUCTION ##################################
################################################################################

# abrir dataset
dataset = 'tweets_003.pkl'
tweets_db = pd.read_pickle(dataset)

tweet_corpus = clean_corpus(tweets_db, 10000)
text_list = tweet_corpus['text'].to_list()


# calcular tf-idf
vectorizer = TfidfVectorizer(stop_words = 'english')
response = vectorizer.fit_transform(text_list)

tf_idf = response.toarray()

# calcular matriz de concenso
CONSENSUS_MATRIX = kmeans_consensus_matrix(tf_idf, 2, 12, 75)

# obtener clasificación de ruido
noise_index_A1 = kmeans_noise(list(tf_idf), CONSENSUS_MATRIX)
noise_index_A2 = dbscan_noise_v1(tf_idf, 0.6, 0.8, 75)
noise_index_A3 = dbscan_noise_v2(tf_idf, CONSENSUS_MATRIX, 0.03, 0.11, 75)

noise_index = true_noise(tf_idf, noise_index_A1, noise_index_A2, noise_index_A3)
In [ ]:
################################################################################
############################## CLUSTERING ######################################
################################################################################

# remover tweets etiquetados como ruido
filtered_tf_idf = list(tf_idf)
filtered_tweets = text_list.copy()

for i in sorted(noise_index, reverse=True):
    del filtered_tweets[i]
    del filtered_tf_idf[i]
    
[cluster_labels, cluster_means] = final_clustering( filtered_tf_idf )
num_clusters = max(cluster_labels) + 1

# ordenar por clusters
cluster_start = []
ordered_tf_idf = filtered_tf_idf.copy()
ordered_tweets = filtered_tweets.copy()

n = 0
for k in range( num_clusters ):
    cluster_start.append(n)
    for i in range( len(cluster_labels) ):
      
        if cluster_labels[i] == k:
            ordered_tf_idf[n] = filtered_tf_idf[i]
            ordered_tweets[n] = filtered_tweets[i]
In [0]:
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA
from sklearn.metrics.pairwise import cosine_similarity

def plot_cluster_similarity(TF_IDF):    
    """
    Grafica la matriz de similitud a partir de los vectores TF-IDF
    correspondientes a los tweets del clustering.
    
    :param list TF-IDF:
        lista de vectores tf-idf de los tweets (ordenados por cluster).
        
    :returns:
        None
    """
    
    # calcular similarity matrix
    SIMILARITY_MATRIX = cosine_similarity(TF_IDF)
    
    fig = plt.figure(figsize=(12, 12))
    plt.matshow(SIMILARITY_MATRIX)
    plt.axis('off')
    plt.show()
    
def plot_PCA_clustering(TF_IDF, cluster_labels):
    """
    Realiza la reducción de dimensionalidad sobre el clustering y grafica el 
    resultado.
    
    :param list TF_IDF:
        lista de vectores tf-idf de los tweets.
    :param list cluster_labels:
        lista con las etiquetas de los clusters al que pertenece cada tweet.
        
    :returns:
        None
    """
    
    # reduccion de dimensionalidad
    X = np.array(TF_IDF)
    
    dim = 2
    reduced_data = PCA(n_components=dim).fit_transform(X)
    
    # plotear
    color_palette = {
                    0: '#fe2260', 1: '#46c1a4', 2: '#1f2269', 3: '#971881',
                    4: '#fed245', 5: '#612ea7', 6: '#5363c8'
                    }
    
    fig, ax = plt.subplots(figsize=(12, 12))
    ax.margins(0.05)
    for i, instance in enumerate(reduced_data):
        k = cluster_labels[i]
        pca_comp_1, pca_comp_2 = reduced_data[i]
        color = color_palette[k]
        
        ax.scatter(pca_comp_1, pca_comp_2, c=color)
    
    plt.show()
In [ ]:
################################################################################
############################### VISUALIZAR #####################################
################################################################################

# matriz de similaridad
plot_cluster_similarity(ordered_tf_idf)


# PCA plotting
plot_PCA_clustering(filtered_tf_idf, cluster_labels)
In [0]:
from sklearn.metrics.pairwise import cosine_similarity

def centroid_tweet(tweets, tf_idf, centroid):
    """
    Retorna el tweet mas similar al centroide del cluster.
    
    :param list tweets:
        lista de tweets del cluster.
    :param list tf_idf:
        lista de vectores tf-idf de los tweets.
    :param list centroids:
        vectores centroide del cluster.
        
    :returns:
        tweet mas similar al centroide del cluster.
    """
    
    SIMILARITY_MATRIX = cosine_similarity(tf_idf, centroid)
    
    max_similarity = np.amax(SIMILARITY_MATRIX)
    index = np.where(SIMILARITY_MATRIX == max_similarity)[0]
    
    return tweets[int(index[0])]
In [ ]:
################################################################################
############################### CENTROIDES #####################################
################################################################################

centroid_tweets = []
for k in range(num_clusters):
    i = cluster_start[k]
    
    # si es el ultimo cluster
    if k == (num_clusters - 1):
        cluster_tweets = ordered_tweets[i:]
        cluster_tf_idf = ordered_tf_idf[i:]
        
    # si no es el ultimo
    else:
        j = cluster_start[k+1]
        cluster_tweets = ordered_tweets[i:j]
        cluster_tf_idf = ordered_tf_idf[i:j]
        
    centroid = [cluster_means[k]]
    tweet = centroid_tweet(cluster_tweets, cluster_tf_idf, centroid)
    centroid_tweets.append(tweet)


print('-'*80)
print('TWEETS CENTROIDES:\n')
for i in range(num_clusters):
    print('CLUSTER ' + str(i) + ' | ' + centroid_tweets[i])
    
print('-'*80 + '\nCLUSTER SAMPLES:')
for i in range(num_clusters):
    print('\nCLUSTER '+ str(i) + ':')
    j = cluster_start[i]
    
    if i == (num_clusters-1):
        tweets = ordered_tweets[j:]
    else:
        tweets = ordered_tweets[j:cluster_start[i+1]]
    random.shuffle(tweets)
    
    for t in tweets[ 0:min(20,len(tweets)) ]:
        print(t)